package lt.inventi.wicket.test; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.NavigableMap; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.regex.Pattern; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.util.lang.Classes; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; /** * @author ogrigas, vplatonov */ public abstract class FuzzyComponentResolverUtils { private FuzzyComponentResolverUtils() { // static utils } /** * Inside the given {@code container}, looks for component whose id equals * {@code path} or whose path ends with {@code path} with optional * intermediaries in the component tree. Search is restricted to the * specified {@code componentType}. * * @return the matching component's path, relative to {@code container} * @throws IllegalArgumentException * if it finds zero or more than one macthing component. */ public static <T> String findComponentPath(MarkupContainer container, String path, Class<T> componentType) { /* * T isn't constrained to the Component hierarchy as sometimes we might be searching using * an interface such as an IFormSubmitter. */ Component component = container.get(path); if (component != null && componentType.isInstance(component)) { return path; } T fuzzyMatch = searchComponentTree(container, path, componentType); return getComponentRelativePath(container, assureFuzzyMatchIsAComponent(container, path, fuzzyMatch)); } /** * Inside the given {@code container}, looks for component whose id equals * {@code path} or whose path ends with {@code path} with optional * intermediaries in the component tree. Search is restricted to the * specified {@code componentType}. * * @return the matching component * @throws IllegalArgumentException * if it finds zero or more than one macthing component. */ public static <T> T findComponent(MarkupContainer container, String path, Class<T> componentType) { Component component = container.get(path); if (component != null && componentType.isInstance(component)) { return componentType.cast(component); } T fuzzyMatch = searchComponentTree(container, path, componentType); assureFuzzyMatchIsAComponent(container, path, fuzzyMatch); return fuzzyMatch; } private static <T> Component assureFuzzyMatchIsAComponent(MarkupContainer container, String path, T fuzzyMatch) { if (!(fuzzyMatch instanceof Component)) { throw new IllegalStateException("Found a non-component " + fuzzyMatch + " in the " + container + " using '" + path + "' path!"); } return (Component) fuzzyMatch; } /** * Returns the {@code component}'s path relative to {@code container}. */ private static String getComponentRelativePath(MarkupContainer container, Component component) { return component.getPath().replaceFirst(container.getPath() + ":", ""); } private static <T> T searchComponentTree(MarkupContainer container, String path, Class<T> componentType) { PathMatchingVisitor<T> visitor = new PathMatchingVisitor<T>(path, componentType); container.visitChildren(componentType, visitor); if (visitor.primaryCandidates.size() == 1) { return visitor.primaryCandidates.iterator().next(); } if (visitor.primaryCandidates.size() > 1) { throw multipleCandidatesException("primary", visitor.primaryCandidates, container, path, componentType); } return selectSecondaryCandidate(container, path, componentType, visitor.secondaryCandidates); } private static <T> T selectSecondaryCandidate(MarkupContainer container, String path, Class<T> componentType, NavigableMap<Match, Set<T>> candidates) { if (candidates.isEmpty()) { throw noCandidatesException(container, path, componentType); } Match last = candidates.lastKey(); SortedMap<Match, Set<T>> bestMatches = candidates.tailMap(Match.lowestFor(last.distance)); if (bestMatches.size() > 1) { throw multipleCandidatesException("secondary", concat(bestMatches.values()), container, path, componentType); } Set<T> equalMatches = bestMatches.values().iterator().next(); if (equalMatches.size() == 1) { return equalMatches.iterator().next(); } throw multipleCandidatesException("secondary", equalMatches, container, path, componentType); } private static <C extends Iterable<T>, T> Iterable<T> concat(Collection<C> xxs) { List<T> result = new LinkedList<T>(); for (Iterable<T> xs: xxs) { for (T x: xs) { result.add(x); } } return result; } private static Match calculateMatch(String[] pathToMatch, String[] searchPath) { // Find the first element in the current component's path which is // equal to the first element in the search path. If no such element exists we // won't consider the path at all. int firstMatchPosition = 0; while (firstMatchPosition < pathToMatch.length && !pathToMatch[firstMatchPosition].equals(searchPath[0])) { firstMatchPosition++; } if (pathToMatch.length == firstMatchPosition) { return null; } String[] toMatch = Arrays.copyOfRange(pathToMatch, firstMatchPosition, pathToMatch.length); int numPartsMatched = 0; for (int i = 0, j = 0; i < toMatch.length && j < searchPath.length; i++) { if (toMatch[i].equals(searchPath[j])) { numPartsMatched++; j++; } } if (numPartsMatched < searchPath.length) { return null; } return new Match(levenshteinDistance(toMatch, searchPath), toMatch.length); } /** * Straight from Wikipedia */ private static int levenshteinDistance(String[] a, String[] b) { int[][] distance = new int[a.length + 1][b.length + 1]; for (int i = 0; i <= a.length; i++) { distance[i][0] = i; } for (int j = 1; j <= b.length; j++) { distance[0][j] = j; } for (int i = 1; i <= a.length; i++) { for (int j = 1; j <= b.length; j++) { distance[i][j] = minimum( distance[i - 1][j] + 1, distance[i][j - 1] + 1, distance[i - 1][j - 1] + ((a[i - 1].equals(b[j - 1])) ? 0 : 1)); } } return distance[a.length][b.length]; } private static int minimum(int a, int b, int c) { return Math.min(Math.min(a, b), c); } private static class Match implements Comparable<Match> { static Match lowestFor(int distance) { return new Match(distance, Integer.MIN_VALUE); } final int distance; /** * Probably should be removed. */ final int matchLength; public Match(int distance, int length) { this.distance = distance; this.matchLength = length; } @Override public int compareTo(Match o) { if (this == o) { return 0; } int distanceResult = compare(o.distance, distance); // the lower, the better int matchLengthResult = compare(matchLength, o.matchLength); // the higher, the better return distanceResult == 0 ? matchLengthResult : distanceResult; } private static int compare(int a, int b) { return (a < b) ? -1 : ((a > b) ? 1 : 0); } @Override public String toString() { return "Match(dst: " + distance + ", mlen:" + matchLength + ")"; } } private static IllegalArgumentException noCandidatesException(MarkupContainer container, String path, Class<?> componentType) { return new IllegalArgumentException( String.format("No %s found with path matching '%s' inside %s (container path: '%s')", Classes.simpleName(componentType), path, Classes.simpleName(container.getClass()), container.getPath())); } private static <T> IllegalArgumentException multipleCandidatesException(String name, Iterable<T> candidates, MarkupContainer container, String path, Class<T> componentType) { StringBuilder message = new StringBuilder() .append(String.format("Multiple %s %ss found with path matching '%s' inside %s (container path: '%s'). ", name, Classes.simpleName(componentType), path, Classes.simpleName(container.getClass()), container.getPath())) .append("Possible candidates:"); for (T c : candidates) { message.append("\n "); if (c instanceof Component) { message.append(((Component) c).getPath()); } else { message.append(c.toString()); } } throw new IllegalArgumentException(message.toString()); } private static class PathMatchingVisitor<T> implements IVisitor<Component, Void> { private static Pattern SPLIT_PATTERN = Pattern.compile(":"); private final String searchPath; private final String[] searchPathParts; private final Class<T> componentType; public final Set<T> primaryCandidates = new HashSet<T>(); public final NavigableMap<Match, Set<T>> secondaryCandidates = new TreeMap<Match, Set<T>>(); public PathMatchingVisitor(String searchPath, Class<T> componentType) { this.searchPath = searchPath; this.searchPathParts = SPLIT_PATTERN.split(searchPath); this.componentType = componentType; } @Override public void component(Component c, IVisit<Void> visit) { String cPath = c.getPath(); boolean idMatches = c.getId().equals(searchPath) || cPath.endsWith(":" + searchPath); if (componentType.isAssignableFrom(c.getClass()) && idMatches) { primaryCandidates.add(componentType.cast(c)); } else { String[] pathToMatch = SPLIT_PATTERN.split(cPath); if (pathToMatch.length > searchPathParts.length) { Match match = calculateMatch(pathToMatch, searchPathParts); if (match != null) { Set<T> matches = secondaryCandidates.get(match); if (matches == null) { matches = new HashSet<T>(); } matches.add(componentType.cast(c)); secondaryCandidates.put(match, matches); } } } } } }